spb/note-spboucher-ai
Public
1import { z } from "zod";2import { handler, parseBody, uuid } from "@/lib/api";3import { db, schema } from "@/db";4import { and, eq, inArray } from "drizzle-orm";5import { audit } from "@/lib/audit";6import { decideProposals } from "@/lib/agents/apply";7import { AGENT_BY_ID } from "@/lib/agents/definitions";8import { parseMeta } from "@/lib/agents/runner";910/** GET → execution detail (live status, tool timeline, documents accessed with titles, proposals, approvals). */11export const GET = handler(async ({ ws, params }) => {12 const id = uuid.parse(params.id);13 const [ex] = await db.select().from(schema.aiExecutions).where(and(eq(schema.aiExecutions.id, id), eq(schema.aiExecutions.workspaceId, ws.id)));14 if (!ex) return Response.json({ error: "Not found" }, { status: 404 });15 const ids = ex.documentsAccessed ?? [];16 const docs = ids.length ? await db.select({ id: schema.pages.id, title: schema.pages.title, icon: schema.pages.icon, section: schema.pages.section, deletedAt: schema.pages.deletedAt }).from(schema.pages).where(inArray(schema.pages.id, ids)) : [];17 const a = AGENT_BY_ID[ex.agent];18 return Response.json({ ...ex, outcome: ex.status === "pending" ? null : ex.outcome, contextPageId: parseMeta(ex.status === "pending" ? ex.outcome : null).pageId ?? null, agentName: a?.name ?? ex.agent, agentIcon: a?.icon ?? "🤖", documents: ids.map((d) => docs.find((x) => x.id === d) ?? { id: d, title: "(unknown page)", icon: null, section: null, deletedAt: null }) });19});20/** POST { action: "approve" | "reject", changes: number[] } | { action: "cancel" } */21export const POST = handler(async ({ req, ws, params, user }) => {22 const id = uuid.parse(params.id);23 const b = await parseBody(req, z.discriminatedUnion("action", [z.object({ action: z.literal("approve"), changes: z.array(z.number().int().min(0)).min(1) }), z.object({ action: z.literal("reject"), changes: z.array(z.number().int().min(0)).min(1) }), z.object({ action: z.literal("cancel") })]));24 if (b.action === "cancel") {25 const [ex] = await db.select({ status: schema.aiExecutions.status, proposedChanges: schema.aiExecutions.proposedChanges }).from(schema.aiExecutions).where(and(eq(schema.aiExecutions.id, id), eq(schema.aiExecutions.workspaceId, ws.id)));26 if (!ex) return Response.json({ error: "Not found" }, { status: 404 });27 if (["pending", "running"].includes(ex.status)) { await db.update(schema.aiExecutions).set({ status: "failed", error: "Cancelled by the owner", outcome: "cancelled", finishedAt: new Date() }).where(eq(schema.aiExecutions.id, id)); }28 else if (ex.status === "awaiting_approval") { const at = new Date().toISOString(); const props = ((ex.proposedChanges ?? []) as any[]).map((p) => (p.status === "proposed" ? { ...p, status: "rejected", decidedAt: at } : p)); await db.update(schema.aiExecutions).set({ status: "rejected", outcome: "all proposals rejected (cancelled)", proposedChanges: props, approvals: props.filter((p) => p.decidedAt === at).map((p) => ({ change: p.idx, approved: false, at })) }).where(eq(schema.aiExecutions.id, id)); }29 else return Response.json({ error: `Cannot cancel a ${ex.status} execution` }, { status: 409 });30 await audit(user.id, "agent.cancel", { type: "execution", id });31 return Response.json({ ok: true });32 }33 const r = await decideProposals(ws, user.id, id, { approve: b.action === "approve" ? b.changes : [], reject: b.action === "reject" ? b.changes : [] });34 return Response.json(r);35});36